circeを使ってJSONとScalaクラスを相互変換する
はい、どーも!CX事業本部の吉田です。
今回は、ScalaでJSONのパースなどによく使われるcirceの使い方をまとめてみました。
circeを使うことで、ScalaのクラスをJSONで出力したり、逆にJSONをパースしてScalaクラスにマッピングする、といったことが簡単にできるようになります。
クラスをJSONにする
まずは単純にScalaのクラスをJSONに出力してみます。次のようにケースクラスと、そのコンパニオンオブジェクトを準備します。
import io.circe.{Encoder} import io.circe.generic.semiauto._ final case class Person(name: String, email: String) object Person { implicit val encoder: Encoder[Person] = deriveEncoder }
クラスをJSONに出力(ここではエンコード)するには、コンパニオンオブジェクト内でエンコーダーをimplicitで準備しておきます。
deriveEncoder
は io.circe.generic.semiauto
にあり、これを使うことで対象のクラスをええ感じにJSONにエンコードしてくれます。
実際にJSONにするには
import io.circe.syntax._ val person = Person("John Doe", "[email protected]") println(person.asJson)
としてやります。ここで io.circe.syntax._
をimportするのを忘れると asJson
メソッドが生えないので注意です。
JSONをパースしてクラスにマッピングする
今度は逆にJSONをパースし、これをクラスにマッピングしてみます。
先程はエンコーダーをimplicitで準備しましたが、今度はデコーダーをコンパニオンオブジェクトに準備します。
object Person { implicit val encoder: Encoder[Person] = deriveEncoder implicit val decoder: Decoder[Person] = deriveDecoder // <-これを追加 }
デコーダーでも、エンコーダーのときと同じように deriveDecoder
を使って、JSONをええ感じにパースしてもらいます。
では実際にJSONをパースしてみましょう。
import io.circe.parser._ val json = """ |{ | "name": "John Doe", | "email": "[email protected]" |} """.stripMargin decode[Person](json) match { case Right(person) => println(person) case Left(error) => println(error) }
io.circe.parser._
にある decode
を使ってJSONをパースします。decodeメソッドはEitherを返すので、これをパターンマッチして、成功時or失敗時で分けて処理できます。
エンコーダーを実装してJSONに出力する
次に、クラスをJSONに出力するときにJSONのフィールド名を変えたいとします。例えばクラス上では email
となっている項目を、JSONでは mail_address
にしたいときなどです。
この場合は deriveEncoder
が使えないので、自前でEncoderを実装してやります。
import io.circe.Json._ object Person { // implicit val encoder: Encoder[Person] = deriveEncoder implicit val encoder: Encoder[Person] = (p: Person) => { obj( "name" -> fromString(p.name), "mail_address" -> fromString(p.email) ) }
io.circe.Json._
にある obj
を使い、引数のPersonを使ってJSONを組み立てる感じですね。
デコーダーを実装してクラスにマッピングする
先のようにエンコーダーを実装した結果、JSONは以下のように出力されます。
{ "name" : "John Doe", "mail_address" : "[email protected]" }
現状では、これをそのままパースしてPersonクラスにマッピングすることはできません。パースできるようにするためには同じくDecoderを自前で実装してやります。
コンパニオンオブジェクトに、以下のようにDecoderを実装します。
// implicit val decoder: Decoder[Person] = deriveDecoder implicit val decoder: Decoder[Person] = (c: HCursor) => { for { name <- c.downField("name").as[String] email <- c.downField("mail_address").as[String] } yield Person(name, email) }
HCursor#downField
でJSONの対象の値が取得できるので、これをfor内包記法で各値を取り出し、yieldでPersonクラスに設定してやります。
また、これを少し応用してやればJSONの値を結合して、その結果をクラスにマッピングするといったこともできますね。例えば以下のようなJSONがあった場合
{ "first_name" : "John", "last_name": "Doe", "mail_address" : "[email protected]" }
first_name
と last_name
を結合した結果をPersonのnameにセットしたい場合は、Decoderを
implicit val decoder: Decoder[Person] = (c: HCursor) => { for { first_name <- c.downField("first_name").as[String] last_name <- c.downField("last_name").as[String] email <- c.downField("mail_address").as[String] } yield Person(s"${first_name} ${last_name}", email) }
とすることで、簡単に対応可能です。
キャメルケースとスネークケースを相互変換する
ここで少し話を戻して deriveEncoder
と deriveDecoder
について。
この2つのエンコーダー&デコーダーは便利なのですが、Scalaクラスの項目名とJSONの項目名が一致している必要があります。
しかし実際には項目名が、Scalaクラスではキャメルケース、JSONはスネークケースといったパターンは多々あると思います。例えば以下のような感じ。
final case class Person(firstName: String, lastName: String, email: String)
{ "first_name" : "John", "last_name": "Doe", "mail_address" : "[email protected]" }
これをいい感じに相互変換するには circe-generic-extras を使うと便利です。
コンパニオンオブジェクトを以下のように修正します。
//import io.circe.generic.semiauto._ <- これをやめる import io.circe.generic.extras.semiauto._ import io.circe.generic.extras.Configuration object Person { implicit val encoder: Encoder[Person] = deriveEncoder implicit val decoder: Decoder[Person] = deriveDecoder implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames }
まずエンコーダーとデコーダーは deriveEncoder
と deriveDecoder
を使うのですが、これは io.circe.generic.semiauto._
ではなく、 io.circe.generic.extras.semiauto._
にあるものを使います。これを忘れると、ただしく相互変換できません。
次に、キャメルケースとスネークケースで相互変換できるよう、上記のように Configuration
をコンパニオンオブジェクトに指定しておきます。
(他に withKebabCaseMemberNames
ケバブケース?!といったものがあります)
これだけで、相互変換できるようになるので、大変便利です。